<template>
  <div id="app">
    <input v-model.number="dataLength" type="text">条 Height:{{ scrollBarHeight }}
    <div ref="scrollsss" class="phantom" @scroll="onScroll">
      <!-- 列表总高 -->
      <ul :style="{height: scrollBarHeight + 'px'}">
        <!-- 列表偏移量 -->
        <Item v-for="item in visibleList" :key="item.brandId" :style="{'transform': `translate3d(0,${scrollTop}px,0)`}" :item="item" :index="item.index" @update-height="updateItemHeight" />
      </ul>
    </div>
  </div>
</template>

<script>
import Item from './Item.vue'
export default {
  name: 'App',
  components: {
    Item
  },
  data() {
    return {
      estimatedItemHeight: 30, // 每一项假定的高度
      visibleCount: 10, // 可视窗口展示项
      dataLength: 100, // 模拟列表项
      startIndex: 0, // 截取数组的 起始 索引
      endIndex: 10, // 截取数组的 结束 索引
      scrollTop: 0, // 距离顶部的偏移量
      scrollBarHeight: 0, // 虚拟列表高度
      bufferItemCount: 4, // 缓冲加载项
      dataList: [], // 数据列表
      itemHeightCache: [], // 每一项高度
      itemTopCache: [] // 每一项距顶部的实际高度
    }
  },
  computed: {
    // 截取要展示的数据
    visibleList() {
      return this.dataList.slice(this.startIndex, this.endIndex + this.bufferItemCount)
    }
  },
  watch: {},
  created() {
    // 初始化demo的列表数据项
    this.dataList = this.getDataList()
    this.generateEstimatedItemData()
  },
  mounted() {},
  methods: {
    generateEstimatedItemData() {
      const estimatedTotalHeight = this.dataList.reduce((pre, current, index) => {
        // 给每一项一个虚拟高度
        this.itemHeightCache[index] = this.estimatedItemHeight
        // 给每一项距顶部的虚拟高度
        this.itemTopCache[index] = index === 0 ? 0 : this.itemTopCache[index - 1] + this.estimatedItemHeight
        return pre + this.estimatedItemHeight
      }, 0)
      // 列表总高
      this.scrollBarHeight = estimatedTotalHeight
    },
    updateItemHeight({ index, height }) {
      // dom元素加载后得到实际高度 重新赋值回去
      this.itemHeightCache[index] = height
      // 重新确定列表的实际总高度
      this.scrollBarHeight = this.itemHeightCache.reduce((pre, current) => {
        return pre + current
      }, 0)
      const newItemTopCache = [0]
      for (let i = 1, l = this.itemHeightCache.length; i < l; i++) {
        // 虚拟每项距顶部高度 + 实际每项高度
        newItemTopCache[i] = this.itemTopCache[i - 1] + this.itemHeightCache[i - 1]
      }
      // 获得每一项距顶部的实际高度
      this.itemTopCache = newItemTopCache
    },
    getDataList() {
      console.log(this.dataLength)
      // 100展开为数组后为每一条数据添加属性
      const newDataList = [...Array(this.dataLength || 0).keys()].map((v, i) => ({
        index: i,
        brandId: i + 1,
        name: `第${i + 1}项`,
        height: Math.floor(Math.max(Math.random() * 10, 5)) * 10
      }))
      console.log(newDataList)
      return newDataList
    },
    // 获取渲染项起始索引
    getStartIndex(scrollTop) {
      // 每一项距顶部的距离
      const arr = this.itemTopCache
      let index = -1
      let left = 0
      let right = arr.length - 1
      let mid = Math.floor((left + right) / 2)
      // 判断 有可循环项时进入
      while (right - left > 1) {
      /*
        二分法：拿每一次获得到的 距顶部距离 scrollTop 同 获得到的模拟每个列表据顶部的距离作比较。
        arr[mid] 为虚拟列高度的中间项
        不断while 循环，利用二分之一将数组分割，减小搜索范围
        直到最终定位到 目标index 值
      */
        // 目标数在左侧
        if (scrollTop < arr[mid]) {
          right = mid
          mid = Math.floor((left + right) / 2)
        } else if (scrollTop > arr[mid]) {
          // 目标数在右侧
          left = mid
          mid = Math.floor((left + right) / 2)
        } else {
          index = mid
          return index
        }
      }
      index = left
      return index
    },
    onScroll() {
      console.log(this.$refs.scrollsss.scrollTop)
      const scrollTop = this.$refs.scrollsss.scrollTop
      console.log('scrollTop', scrollTop)
      const startIndex = this.getStartIndex(scrollTop)
      // 如果是奇数开始，就取其前一位偶数
      if (startIndex % 2 !== 0) {
        this.startIndex = startIndex - 1
      } else {
        this.startIndex = startIndex
      }
      this.endIndex = this.startIndex + this.visibleCount
      this.scrollTop = this.itemTopCache[this.startIndex] || 0
    }
  }
}
</script>

<style lang="less" scoped>
.phantom {
  border: solid 1px #eee;
  margin-top: 10px;
  height: 600px;
  overflow: auto
}

ul {
  background: #ccc;
  list-style: none;
  padding: 0;
  margin: 0;
   li {
    outline: solid 1px #fff;
    &:nth-child(2n) {
      background: #fff;
    }
  }
}
</style>
